メディア展開データの変数間の関係を見る#

メディア展開データを対象に、2章で取り上げた変数間の関係を見るための可視化手法を再度適用します。

これまでの学びを振り返り、知識の定着を図りましょう。

初期設定#

以降では、マンガ・アニメ・ゲームデータを可視化するための初期設定を行います。 なお、紙幅の都合のため、書籍版と一部構成が異なることにご注意ください。

Import#

必要なライブラリをImportします。

Hide code cell content
# warningsモジュールのインポート
import warnings

# データ解析や機械学習のライブラリ使用時の警告を非表示にする目的で警告を無視
# 本書の文脈では、可視化の学習に議論を集中させるために選択した
# ただし、学習以外の場面で、警告を無視する設定は推奨しない
warnings.filterwarnings("ignore")
Hide code cell content
# itertoolsモジュールのインポート
# 効率的なループを実行するためのイテレータビルディングブロックを提供
# これにより、データのコンビネーションや順列などを簡潔に表現できる
import itertools

# pathlibモジュールのインポート
# ファイルシステムのパスを扱う
from pathlib import Path

# numpy:数値計算ライブラリのインポート
# npという名前で参照可能
import numpy as np

# pandas:データ解析ライブラリのインポート
# pdという名前で参照可能
import pandas as pd

# plotly.expressのインポート
# インタラクティブなグラフ作成のライブラリ
# pxという名前で参照可能
import plotly.express as px

# plotly.graph_objectsのインポート
# より詳細なグラフ作成機能を利用可能
# goという名前で参照可能
import plotly.graph_objects as go

# plotly.graph_objectsからFigureクラスのインポート
# 型ヒントの利用を主目的とする
from plotly.graph_objects import Figure

# plotly.subplotsからmake_subplotsのインポート
# 複数のサブプロットを含む複合的な図を作成する際に使用
from plotly.subplots import make_subplots

なお、型ヒントについてはこちらを参照ください。

定数#

本Notebookで用いる定数を定義します。 なお、Pythonにおける定数の扱いについては、こちらを参照ください。

Hide code cell content
# メディア展開データが保存されているディレクトリのパス
DIR_IN = Path("../../data/mix/input/")

# 分析結果の出力先ディレクトリのパス
DIR_OUT = DIR_IN.parent / "output" / Path.cwd().parts[-1] / "assocs"
Hide code cell content
# 読み込み対象ファイル名の定義

# アニメ各話と原作マンガの作者の対応関係に関するファイル
FN_AE_CRT = "mix_ae_crt.csv"

# マンガ各話とアニメ作品の対応関係に関するファイル
FN_CE_AC = "mix_ce_ac.csv"
Hide code cell content
# plotlyの描画設定の定義

# plotlyのグラフ描画用レンダラーの定義
# Jupyter Notebook環境のグラフ表示に適切なものを選択
RENDERER = "plotly_mimetype+notebook"
Hide code cell content
# 質的変数の描画用のカラースケールの定義

# Okabe and Ito (2008)基準のカラーパレット
# 色の識別性が高く、多様な色覚の人々にも見やすい色組み合わせ
# 参考URL: https://jfly.uni-koeln.de/color/#pallet
OKABE_ITO = [
    "#000000",  # 黒 (Black)
    "#E69F00",  # 橙 (Orange)
    "#56B4E9",  # 薄青 (Sky Blue)
    "#009E73",  # 青緑 (Bluish Green)
    "#F0E442",  # 黄色 (Yellow)
    "#0072B2",  # 青 (Blue)
    "#D55E00",  # 赤紫 (Vermilion)
    "#CC79A7",  # 紫 (Reddish Purple)
]

関数#

本Notebookで用いる関数を定義します。

Hide code cell content
def show_fig(fig: Figure) -> None:
    """
    所定のレンダラーを用いてplotlyの図を表示
    Jupyter Bookなどの環境での正確な表示を目的とする

    Parameters
    ----------
    fig : Figure
        表示対象のplotly図

    Returns
    -------
    None
    """

    # 図の周囲の余白を設定
    # t: 上余白
    # l: 左余白
    # r: 右余白
    # b: 下余白
    fig.update_layout(margin=dict(t=25, l=25, r=25, b=25))

    # 所定のレンダラーで図を表示
    fig.show(renderer=RENDERER)
Hide code cell content
def add_years_to_df(
    df: pd.DataFrame, unit_years: int = 10, col_date: str = "date"
) -> pd.DataFrame:
    """
    データフレームにunit_years単位で区切った年数を示す新しい列を追加

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    unit_years : int, optional
        年数を区切る単位、デフォルトは10
    col_date : str, optional
        日付を含むカラム名、デフォルトは "date"

    Returns
    -------
    pd.DataFrame
        新しい列が追加されたデータフレーム
    """

    # 入力データフレームをコピー
    df_new = df.copy()

    # unit_years単位で年数を区切り、新しい列として追加
    df_new["years"] = (
        pd.to_datetime(df_new[col_date]).dt.year // unit_years * unit_years
    )

    # 'years'列のデータ型を文字列に変更
    df_new["years"] = df_new["years"].astype(str)

    return df_new
Hide code cell content
def resample_df_by_col_and_years(df: pd.DataFrame, col: str) -> pd.DataFrame:
    """
    指定されたカラムと年数に基づき、データフレームを再サンプル
    colとyearsの全ての組み合わせが存在するように0埋めを行う
    この処理は、作図時にX軸方向の順序が変わることを防ぐために必要

    Parameters
    ----------
    df : pd.DataFrame
        入力データフレーム
    col : str
        サンプリング対象のカラム名

    Returns
    -------
    pd.DataFrame
        再サンプルされたデータフレーム
    """

    # 入力データフレームを新しい変数にコピー
    df_new = df.copy()

    # データフレームからユニークな年数一覧を取得
    unique_years = df["years"].unique()

    # データフレームからユニークなcolの値一覧を取得
    unique_vals = df[col].unique()

    # 一意なカラムの値と年数の全ての組み合わせに対して処理
    for val, years in itertools.product(unique_vals, unique_years):
        # 対象のカラムの値と年数が一致するデータを抽出
        df_tmp = df_new[(df_new[col] == val) & (df_new["years"] == years)]

        # 該当するデータが存在しない場合
        if df_tmp.shape[0] == 0:
            # 0埋めのデータを作成
            default_data = {c: 0 for c in df_tmp.columns}
            # col列についてはvalで埋める
            default_data[col] = val
            # years列についてはyearで埋める
            default_data["years"] = years
            # 新たなレコードとして追加
            df_add = pd.DataFrame(default_data, index=[0])

            # 0埋めのデータをデータフレームに追加
            df_new = pd.concat([df_new, df_add], ignore_index=True)

    return df_new
Hide code cell content
def create_connectedplot(
    df: pd.DataFrame, x: str, y: str, text: str, color: str, **args
) -> Figure:
    """
    データフレームから連結散布図を作成する

    Parameters
    ----------
    df : pd.DataFrame
        グラフに使用するデータを含むpandasデータフレーム
    x : str
        x軸に使用するデータフレームの列名
    y : str
        y軸に使用するデータフレームの列名
    text : str
        プロット上で表示するテキストを含むデータフレームの列名
    color : str
        プロットのカラーマッピングに使用されるデータフレームの列名
        カラーバーのタイトルとしても使用される
    **args
        追加のキーワード引数。これらはPlotly Express関数に渡される

    Returns
    -------
    fig : plotly.graph_objs._figure.Figure
        生成されたPlotlyのFigureオブジェクト
    """

    # 折れ線グラフを使って連結散布図の基礎を作成する
    fig_line = px.line(df, x=x, y=y, text=text, **args)

    # 散布図を作成し、連結散布図のポイントとして折れ線グラフに追加する
    fig_scatter = px.scatter(df, x=x, y=y, color=color, **args)

    # 散布図のトレースを折れ線グラフに追加し、接続点を表現する
    for trace in fig_scatter.data:
        fig_line.add_trace(trace)

    # 折れ線グラフのスタイルを更新し、視覚的特徴を強化する
    fig_line.update_traces(
        line={"color": "grey"},
        marker={"size": 15, "line_width": 1, "opacity": 0.7},
        textposition="bottom right",
    )

    # グラフのレイアウトを更新し、カラーバーのタイトルを`color`引数に基づいて設定する
    fig_line.update_layout(coloraxis_colorbar=dict(title=color))

    return fig_line
Hide code cell content
def save_df_to_csv(df: pd.DataFrame, dir_save: Path, fn_save: str) -> None:
    """
    DataFrameをCSVファイルとして指定されたディレクトリに保存する関数

    Parameters
    ----------
    df : pd.DataFrame
        保存対象となるDataFrame
    dir_save : Path
        出力先ディレクトリのパス
    fn_save : str
        保存するCSVファイルの名前(拡張子は含めない)
    """
    # 出力先ディレクトリが存在しない場合は作成
    dir_save.mkdir(parents=True, exist_ok=True)

    # 出力先のパスを作成
    p_save = dir_save / f"{fn_save}.csv"

    # DataFrameをCSVファイルとして保存する
    df.to_csv(p_save, index=False, encoding="utf-8-sig")

    # 保存完了のメッセージを表示する
    print(f"DataFrame is saved as '{p_save}'.")

可視化例#

まず、可視化対象となるデータを読み込みましょう。

Hide code cell content
# pandasのread_csv関数でCSVファイルの読み込み
df_ce_ac = pd.read_csv(DIR_IN / FN_CE_AC)

散布図#

ここまでの可視化によって、アニメ化に至ったマンガ作品(アニメ化作品)に関するいくつかの仮説を立てることができました:

  • アニメ化作品は5話目以降の掲載位置の分布が異なる(かもしれない[1]

  • アニメ化作品は全体の1割程度しか存在しない(かもしれない)

  • アニメ化作品の比率はマンガ雑誌によって異なる(かもしれない)

  • アニメ化作品の比率は連載開始年によって異なる(かもしれない)

  • アニメ化作品のほとんどは、最終的に合計各話数が長くなる(かもしれない)

本項ではより踏み込んで、連載初期の統計量から、アニメ化に至るかどうかを予測することが可能(かもしれない)か考えます。 具体的には、散布図を用いて「連載初期の平均掲載位置と平均ページ数からアニメ化に至るかどうか予測可能である」という仮説を確認します。 掲載位置が巻頭に近いほど、またページ数が多いほど、読者の目につきやすいと考えられます。 それぞれ雑誌内で限られた資源ですので、これらによりマンガ作品の人気や編集部の期待を測ることができるのでは、という目論見で仮説を設定しました。

散布図Scatter ) とは、主に二つの量的変数に対して、一つ一つのデータをマーカーの 位置 で表す可視化手法でした。 マーカーの で三つ目の変数を表現することもあります。 詳細は8章にありますので、適宜復習に役立ててください。

Hide code cell content
# 最小のマンガ各話数を設定
min_nce = 8

# df_ce_acデータフレームを日付で昇順に並び替え、新しいインデックスを割り当て
df_ce_ac = df_ce_ac.sort_values("date", ignore_index=True)

# 'first_date_cc'列のデータを日付型に変換
df_ce_ac["first_date_cc"] = pd.to_datetime(df_ce_ac["first_date_cc"])

# n_ceがmin_nce以上かつfirst_date_ccの年が1990年以降のレコードをフィルタリング
df_tmp = df_ce_ac[
    (df_ce_ac["n_ce"] >= min_nce) & (df_ce_ac["first_date_cc"].dt.year >= 1990)
].reset_index(drop=True)

# 各ccidについて、最初のmin_nce個のレコードのみを保持
df_tmp = df_tmp.groupby("ccid").head(min_nce).reset_index(drop=True)

# 'acid'列に欠損値がない場合(アニメーションが存在する場合)にTrueを設定し、
# 新しい列'is_animated'をデータフレームに追加
df_tmp["is_animated"] = ~df_tmp["acid"].isna()
Hide code cell content
# df_tmpデータフレームを'mcname'、'ccid'、'ccname'でグループ化し、
# それぞれのグループにおける以下の列の平均値を計算:
# 'page_start_position'、'pages'、'four_colored'、'n_ce'、
# 'is_animated'、'first_date_cc'
df_scat = (
    df_tmp.groupby(["mcname", "ccid", "ccname"])[
        [
            "page_start_position",
            "pages",
            "four_colored",
            "n_ce",
            "is_animated",
            "first_date_cc",
        ]
    ]
    .mean()
    .reset_index()
)

# 'is_animated'列が1の場合はTrueに、それ以外の場合はFalseに変換
# これにより、'is_animated'列を論理値(TrueまたはFalse)で表現
df_scat["is_animated"] = df_scat["is_animated"] == 1

# データフレームの列名をより分かりやすい名称に変更
df_scat = df_scat.rename(
    columns={
        "mcname": "マンガ雑誌名",
        "ccname": "マンガ作品名",
        "page_start_position": f"{min_nce}話目までの平均掲載位置",
        "pages": f"{min_nce}話目までの平均ページ数",
        "four_colored": f"{min_nce}話目までのカラー獲得率",
        "n_ce": "合計各話数",
        "is_animated": "アニメ化",
        "first_date_cc": "連載開始日",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_scat.head()
マンガ雑誌名 ccid マンガ作品名 8話目までの平均掲載位置 8話目までの平均ページ数 8話目までのカラー獲得率 合計各話数 アニメ化 連載開始日
0 週刊少年サンデー C110901 戦争劇場 0.397316 9.50 0.125 55.0 False 2014-08-06
1 週刊少年サンデー C110913 デジコン 0.237189 30.25 0.375 26.0 False 2014-09-03
2 週刊少年サンデー C110925 おいしい神しゃま 0.654687 4.00 1.000 39.0 False 2014-09-24
3 週刊少年サンデー C110985 トキワ来たれり!! 0.172369 23.25 0.125 120.0 False 2014-12-03
4 週刊少年サンデー C110987 ドリー・マー 0.319380 27.25 0.250 32.0 False 2014-12-10
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_scat, DIR_OUT, "scat")
DataFrame is saved as '../../data/mix/output/09/assocs/scat.csv'.
Hide code cell source
# Plotly Expressを使用して、df_scatデータフレームから散布図を作成
# x軸にはmin_nce話までの平均掲載位置を、y軸にはmin_nce話までの平均ページ数を設定
# 'アニメ化'の値によってOKABE_ITOスケールで色分けし、マーカーの透明度を0.7に設定
# 各マーカーにマウスオーバーした際に表示されるテキストとして、マンガ作品名を指定
fig = px.scatter(
    df_scat,
    x=f"{min_nce}話目までの平均掲載位置",
    y=f"{min_nce}話目までの平均ページ数",
    color="アニメ化",
    opacity=0.7,
    hover_name="マンガ作品名",
    color_discrete_sequence=OKABE_ITO,
)

# 散布図のマーカーをカスタマイズ
# マーカーのサイズを15に設定し、枠線の幅を1、枠線の色を白に設定
fig.update_traces(
    marker={
        "size": 15,
        "line_width": 1,
        "line_color": "white",
    }
)

# 作成した散布図を表示する関数を呼び出し
show_fig(fig)

上図は、マンガ作品の8話目までの平均掲載位置と平均ページ数、そして将来的にアニメ化に至るか否かを表現した散布図です。 1990年以降に連載を開始し、かつ8話以上継続したマンガ作品を可視化対象としています。 アニメ化に至ったマンガ作品をオレンジ色(True)、そうでない作品を黒色(False)で表現しています。

視覚的な印象として、アニメ化実績の有無によって平均掲載位置や平均ページ数の分布は大きく変わらないように見えます。 強いて言えば、アニメ化実績のあるマンガ作品のほうが平均掲載位置が前方よりな気はしますが、マーカーの重複も多いため確信は持てません。

では、マンガ雑誌ごとに分けてプロットするとどうでしょうか?

Hide code cell source
# Plotly Expressを使用して、df_scatデータフレームから散布図を作成
# x軸にはmin_nce話までの平均掲載位置を、y軸にはmin_nce話までの平均ページ数を設定
# 'アニメ化'の値によってOKABE_ITOスケールで色分けし、マーカーの透明度を0.7に設定
# 各マーカーにマウスオーバーした際に表示されるテキストとして、マンガ作品名を指定
# マンガ雑誌名を基準にファセットを分け、facet_col_wrapで列数を指定
fig = px.scatter(
    df_scat,
    x=f"{min_nce}話目までの平均掲載位置",
    y=f"{min_nce}話目までの平均ページ数",
    color="アニメ化",
    opacity=0.7,
    hover_name="マンガ作品名",
    color_discrete_sequence=OKABE_ITO,
    facet_col="マンガ雑誌名",
    facet_col_wrap=2,
)

# 散布図のマーカーをカスタマイズ
# マーカーのサイズを15に設定し、枠線の幅を1、枠線の色を白に設定
fig.update_traces(
    marker={
        "size": 15,
        "line_width": 1,
        "line_color": "white",
    }
)

# ファセットごとのタイトルを簡略化するため、=以降のみを抜き出して表示
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 可視化結果を再表示
show_fig(fig)

上図は、マンガ作品の8話目までの平均掲載位置と平均ページ数、そして将来的にアニメ化に至るか否かを表現した散布図をマンガ雑誌別に作成したものです。 1990年以降に連載を開始し、かつ8話以上継続したマンガ作品を可視化対象としています。 アニメ化に至ったマンガ作品をオレンジ色(True)、そうでない作品を黒色(False)で表現しています。

マンガ雑誌によって特徴があるように見えます。 例えば週刊少年ジャンプは他誌と比較して平均掲載位置、平均ページ数ともに特定の領域に集中していますが、アニメ化実績のある作品はさらに狭い領域に偏在しています。 週刊少年マガジンは平均ページ数が20-30ページ付近のマンガ作品と、10ページ以下のマンガ作品で分布がはっきりと分かれているように見えて非常に興味深いです。 前者は平均掲載位置が0.6以上に集中おり、逆に後者は0.3以降に集中しています。 マンガ雑誌としての掲載順のポリシーを伺い知るヒントとなりそうです。 週刊少年チャンピオンは、他誌と比較して平均ページ数の上限が厳密に決まっているように見えます。 具体的には、平均ページ数が30以上のマンガ作品が極端に少ないようです。

しかし残念ながら、どのマンガ雑誌においても、アニメ化に至る作品とそうでない作品を完全に分離することは難しそうに見えます。 「連載初期の平均掲載位置と平均ページ数からアニメ化に至るかどうか予測可能である」という仮説の雲行きは怪しそうです。

バブルチャート#

散布図で様子を見ましたが、「連載初期の平均掲載位置と平均ページ数からアニメ化に至るかどうか予測可能である」という仮説を支持する根拠となるような結果は得られませんでした。 それでは、変数を追加して別の角度から仮説を確認してみましょう。 具体的には、バブルチャートを用いて「連載初期の平均掲載位置と平均ページ数と カラー獲得率 からアニメ化に至るかどうか予測可能である」という仮説を確認します。

バブルチャートBubble Chart ) とは、散布図を拡張し、 マーカーの大きさ を利用することで新たな量的変数を表現する可視化手法でした。 ただし、「マーカーの大きさ」に関しては、(全体的な傾向は掴めるものの)個別の比較は難しいことにご注意ください。 詳細は8章にありますので、復習に役立てましょう。

Hide code cell content
# 散布図と同じデータを利用
df_bub = df_scat.copy()
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_bub, DIR_OUT, "bub")
DataFrame is saved as '../../data/mix/output/09/assocs/bub.csv'.
Hide code cell source
# Plotly Expressを使用して、df_bubデータフレームからバブルチャートを作成
# x軸には最初のmin_nce話までの平均掲載位置を、y軸には最初のmin_nce話までの平均ページ数を設定
# マーカーのサイズを最初のmin_nce話までのカラー獲得率に基づいて設定し、
# 'アニメ化'の値によってOKABE_ITOに基づき色分けを行い、透明度を0.7に設定
# 各マーカーにマウスオーバーした際に表示されるテキストとして、マンガ作品名を指定
fig = px.scatter(
    df_bub,
    x=f"{min_nce}話目までの平均掲載位置",
    y=f"{min_nce}話目までの平均ページ数",
    size=f"{min_nce}話目までのカラー獲得率",
    color="アニメ化",
    opacity=0.7,
    hover_name="マンガ作品名",
    color_discrete_sequence=OKABE_ITO,
)

# バブルチャートのマーカーをカスタマイズ
# マーカーの枠線の幅を1、枠線の色を白に設定
fig.update_traces(
    marker={
        "line_width": 1,
        "line_color": "white",
    }
)

# 作成したバブルチャートを表示する関数を呼び出し
show_fig(fig)

上図は、マンガ作品の8話目までの平均掲載位置と平均ページ数、そして将来的にアニメ化に至るか否かを表現した散布図です。 1990年以降に連載を開始し、かつ8話以上継続したマンガ作品を可視化対象としています。 アニメ化に至ったマンガ作品をオレンジ色(True)、そうでない作品を黒色(False)で表現しています。 また、マーカーの大きさは、8話目までのカラー獲得率と対応しています。

カラー獲得率を追加してはみましたが、少なくとも人間の目でアニメ化実績のある作品とそうでない作品を識別することは容易ではありません。 では、マンガ雑誌別にプロットするとどうでしょうか?

Hide code cell source
# Plotly Expressを使用して、df_bubデータフレームからバブルチャートを作成
# x軸には最初のmin_nce話までの平均掲載位置を、y軸には最初のmin_nce話までの平均ページ数を設定
# マーカーのサイズを最初のmin_nce話までのカラー獲得率に基づいて設定し、
# 'アニメ化'の値によってOKABE_ITOに基づき色分けを行い、透明度を0.7に設定
# 各マーカーにマウスオーバーした際に表示されるテキストとして、マンガ作品名を指定
# マンガ雑誌名に基づきファセットを分け、2列で表示
fig = px.scatter(
    df_bub,
    x=f"{min_nce}話目までの平均掲載位置",
    y=f"{min_nce}話目までの平均ページ数",
    size=f"{min_nce}話目までのカラー獲得率",
    color="アニメ化",
    opacity=0.7,
    hover_name="マンガ作品名",
    color_discrete_sequence=OKABE_ITO,
    facet_col="マンガ雑誌名",
    facet_col_wrap=2,
)

# バブルチャートのマーカーをカスタマイズ
# マーカーの枠線の幅を1、枠線の色を白に設定
fig.update_traces(
    marker={
        "line_width": 1,
        "line_color": "white",
    }
)

# ファセットのタイトルを「=」以降(つまり具体的なマンガ雑誌名)のみに簡略化
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成したバブルチャートを表示する関数を呼び出し
show_fig(fig)

上図は、マンガ作品の8話目までの平均掲載位置と平均ページ数、そして将来的にアニメ化に至るか否かを表現した散布図をマンガ雑誌別に作成したものです。 1990年以降に連載を開始し、かつ8話以上継続したマンガ作品を可視化対象としています。 アニメ化に至ったマンガ作品をオレンジ色(True)、そうでない作品を黒色(False)で表現しています。 また、マーカーの大きさは8話目までのカラー獲得率と対応しています。

特に外れ値について、マンガ雑誌別の特徴があらわれたように見えます。 例えば週刊少年ジャンプにおいて平均ページ数が非常に多かった3作品のうち2作品(BORUTO-ボルト- -NARUTO NEXT GENERATIONS-トリコ)は、カラー獲得率も非常に高いことがわかりました。 週刊少年チャンピオンでは、極端に平均掲載位置が小さい(つまり巻頭に寄っている)3作品のすべて(聖闘士星矢 NEXT DIMENSION 冥王神話範馬刃牙 SON OF OGRE刃牙道)も同様に、カラー獲得率が非常に高いことがわかりました。 これ以外にも、極端な平均掲載位置やページ数を持つマンガ作品の中に、同様に極端なカラー獲得率を持つものがありました。 何らかの事情で特別な扱い(あるいは期待)を受けていた作品と解釈できるかもしれません。

特定のマンガ作品に対する解釈性は高まりましたが、アニメ化に至る作品とそうでない作品を分類するに足る情報かと言われると自信が持てません。 この可視化結果だけでは、「連載初期の平均掲載位置と平均ページ数とカラー獲得率からアニメ化に至るかどうか予測可能である」という仮説を支持する根拠は得られませんでした。

最後にモーションチャートについても復習をしておきましょう。

Hide code cell content
# df_ce_acデータフレームから、n_ceがmin_nce以上かつfirst_date_ccの年が1990年以降のレコードを選択
df_tmp = df_ce_ac[
    (df_ce_ac["n_ce"] >= min_nce) & (df_ce_ac["first_date_cc"].dt.year >= 1990)
].reset_index(drop=True)

# 各マンガ作品ID(ccid)ごとに、最初のmin_nce個のレコードのみを保持
df_tmp = df_tmp.groupby("ccid").head(min_nce).reset_index(drop=True)

# 'acid'列に欠損値がない場合にTrueを設定し、新しい列'is_animated'をデータフレームに追加
df_tmp["is_animated"] = ~df_tmp["acid"].isna()
Hide code cell content
# df_tmpデータフレームを'mccid'と'date'で昇順に並び替え、新しいインデックスを割り当て
df_motion = df_tmp.sort_values(["ccid", "date"], ignore_index=True)

# 各マンガ作品ID('ccid')ごとに連続したエピソード番号('ceno')を割り当て
df_motion["ceno"] = df_motion.groupby("ccid").cumcount() + 1

# 各マンガ作品から最初のmin_nce話までのデータのみを保持
df_motion = df_motion[df_motion["ceno"] <= min_nce].reset_index(drop=True)

# 'acid'列に欠損値がない場合にTrueを設定し、新しい列'is_animated'をデータフレームに追加
df_motion["is_animated"] = ~df_motion["acid"].isna()

# 'mcname'(マンガ雑誌名)、'is_animated'(アニメ化されているかどうか)、
# 'ceno'(エピソード番号)でグループ化し、平均掲載位置、平均ページ数、カラー獲得率、作品数を集計
df_motion = (
    df_motion.groupby(["mcname", "is_animated", "ceno"])
    .agg(
        平均掲載位置=("page_start_position", "mean"),
        平均ページ数=("pages", "mean"),
        カラー獲得率=("four_colored", "mean"),
        作品数=("ceno", "size"),
    )
    .reset_index()
)

# データフレームの列名をより分かりやすい名称に変更
df_motion = df_motion.rename(
    columns={
        "mcname": "マンガ雑誌名",
        "is_animated": "アニメ化",
        "ceno": "話数",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_motion.head()
マンガ雑誌名 アニメ化 話数 平均掲載位置 平均ページ数 カラー獲得率 作品数
0 週刊少年サンデー False 1 0.145375 42.392157 0.725490 255
1 週刊少年サンデー False 2 0.276159 26.811765 0.168627 255
2 週刊少年サンデー False 3 0.323798 20.584314 0.125490 255
3 週刊少年サンデー False 4 0.355677 18.094118 0.129412 255
4 週刊少年サンデー False 5 0.390275 17.494118 0.098039 255
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_motion, DIR_OUT, "motion")
DataFrame is saved as '../../data/mix/output/09/assocs/motion.csv'.
Hide code cell source
# Plotly Expressを使用して、df_motionデータフレームからモーションチャートを作成
# x軸に平均掲載位置、y軸にカラー獲得率を設定し、色分けはアニメ化されているかどうかで行う
# マンガ雑誌名ごとに異なるプロットに分け、各プロットのサイズは作品数に基づく
# 各マンガ雑誌名のプロットは2列でラップされ、話数ごとにアニメーションする
# x軸、y軸の表示範囲を固定し、カラースケールとしてOKABE_ITOを選択
fig = px.scatter(
    df_motion,
    x="平均掲載位置",
    y="カラー獲得率",
    color="アニメ化",
    facet_col="マンガ雑誌名",
    size="作品数",
    facet_col_wrap=2,
    animation_frame="話数",
    range_x=[0, 1],
    range_y=[0, 1],
    animation_group="アニメ化",
    color_discrete_sequence=OKABE_ITO,
    height=600,
)

# 各ファセットのタイトルをマンガ雑誌名のみに変更("マンガ雑誌名="を取り除く)
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成したモーションチャートを表示する関数を呼び出し
show_fig(fig)

上図は、特定の話数ごとの、アニメ化実績のあるマンガ作品とそうでない作品の平均掲載位置とカラー獲得率を表現したモーションチャートです。 マンガ雑誌ごとにファセットをわけており、マーカーの大きさは該当するマンガ作品数を表しています。 また、1990年以降に連載を開始し、8話以上継続したマンガ作品を集計対象としています。

▶ボタンを押下することで、各話数ごとの指標値の推移をアニメーションとして確認できます。 大まかな傾向として、4話ごろまでアニメ化作品とそうでない作品に差は見られませんが、5話以降わずかに挙動が異なるように見えます。

散布図行列#

バブルチャートでは、「連載初期の平均掲載位置と平均ページ数とカラー獲得率からアニメ化に至るかどうか予測可能である」という仮説を確認しましたが、これを支持する根拠となるような結果は得られませんでした。 バブルチャートの技術的な制約の一つとして、マーカーのサイズに対応付けられた変数の量的な比較がし辛いというものがあります。 この制約を回避する可視化手法の一つとして散布図行列があります。 本項では散布図行列を用いて、「連載初期の各種指標からアニメ化に至るかどうか予測可能である」という仮説を確認しましょう。

散布図行列Scatter Matrix ) とは、複数の量的変数に対して、全ての組合せの散布図を 行列 上に配置する可視化手法でした。 対角成分をヒストグラム密度プロットにする実装もあります。 一つ一つの図が小さくなってしまいますが、三つ以上の量的変数の関係を把握する際に便利です。 詳細は8章にありますので、復習に役立てましょう。

Hide code cell content
# df_scatデータフレームのコピーを作成してdf_matに格納
df_mat = df_scat.copy()

# df_matデータフレームの列名を変更
# 散布図行列上に表示するため、各列名から「min_nce話目までの」を削除して簡略化
# 例えば、「8話目までの平均掲載位置」が「平均掲載位置」になる
df_mat.columns = [c.replace(f"{min_nce}話目までの", "") for c in df_mat.columns]
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_mat, DIR_OUT, "mat")
DataFrame is saved as '../../data/mix/output/09/assocs/mat.csv'.
Hide code cell source
# Plotly Expressを使用して、df_matデータフレームから散布図行列を作成
# '平均掲載位置'、'平均ページ数'、'カラー獲得率'、'連載開始日'を次元として使用し、
# 'アニメ化'の値に応じてOKABE_ITOに基づき色分けを行い、透明度を0.6に設定
# 各マーカーにマウスオーバーした際に表示されるテキストとして、マンガ作品名を指定
fig = px.scatter_matrix(
    df_mat,
    dimensions=["平均掲載位置", "平均ページ数", "カラー獲得率", "連載開始日"],
    color="アニメ化",
    opacity=0.6,
    height=600,
    hover_name="マンガ作品名",
    color_discrete_sequence=OKABE_ITO,
)

# 散布図行列のマーカーをカスタマイズ
# マーカーの枠線の幅を1、枠線の色を白に設定
fig.update_traces(
    marker={
        "line_width": 1,
        "line_color": "white",
    }
)

# 作成した散布図行列を表示する関数を呼び出し
show_fig(fig)

上図は、アニメ化に至ったマンガ作品とそうでない作品の各種指標値の分布を表現した散布図行列です。 マーカーの色がアニメ化実績の有無を表現しており、オレンジ色(True)がアニメ化に至った作品、黒色(False)がそうでない作品を表しています。 また、1990年以降に連載を開始し、合計8話以上継続したマンガ作品を可視化対象としました。

8話目までの平均掲載位置、平均ページ数、カラー獲得率、そして(おまけとして)連載開始日の全ての組合せに対して散布図を作成しました。 ちなみに、散布図で取り上げた例は、上図の2行1列目に該当します。 「連載初期の各種指標からアニメ化に至るかどうか予測可能である」かどうか確認する最も素朴な方法は、上図の中でオレンジ色(アニメ化実績のあるマンガ作品)と黒色(そうでないマンガ作品)のマーカーを分離可能な散布図があるかどうか確認することです。 それぞれの散布図は確かに興味深いですが、両者を区別できるような変数の組合せは見当たりません。

なお、素朴ではない確認方法として、例えば数理統計学や機械学習の枠組みに従う手続きがありますが、本書のスコープ外のため割愛します。

二次元ヒストグラム#

散布図バブルチャートで「連載初期の平均掲載位置と平均ページ数からアニメ化に至るかどうか予測可能である」という仮説を確認しましたが、良い結果は得られませんでした。 これらの手法では全てのレコードをマーカーとして表現するため、データサイズが大きいときに重複生じてしまい、変数の分布がわかりづらくなるという課題がありました。 そこで本項では、二次元ヒストグラムを用いてこの課題を回避し、仮説を再度確認します。

二次元ヒストグラム2D Bins ) とは、 二種類 の量的変数を対象として、その分布を で表現する可視化手法でした。 文字通りヒストグラムの二次元版と捉えることもできますし、 ヒートマップの一種と捉えることもできます。 詳細は8章にありますので、復習に役立てましょう。

Hide code cell content
# 散布図と同じデータを利用
df_2d = df_scat.copy()
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_2d, DIR_OUT, "2d")
DataFrame is saved as '../../data/mix/output/09/assocs/2d.csv'.
Hide code cell source
# 最初のmin_nce話までの平均掲載位置と平均ページ数を表す列名を変数に格納
col_x = f"{min_nce}話目までの平均掲載位置"
col_y = f"{min_nce}話目までの平均ページ数"

# Plotly Expressを使用して、df_2dデータフレームから密度ヒートマップを作成
# x軸には平均掲載位置、y軸には平均ページ数を設定
# 'アニメ化'ごとに異なるヒートマップに分け、2列でラップして表示
fig = px.density_heatmap(
    df_2d, x=col_x, y=col_y, facet_col="アニメ化", facet_col_wrap=2
)

# 作成した密度ヒートマップを表示する関数を呼び出し
show_fig(fig)

上図は、マンガ作品の8話目までの平均掲載位置と平均ページ数の分布を、アニメ化実績の有無で分割して表示した二次元ヒストグラムです。 左側がアニメ化実績のない作品、右側がアニメ化実績のある作品の分布です。 色が明るい領域ほど、多くのマンガ作品が該当することを表します。 なお、1990年以降に連載を開始し、合計8話以上継続したマンガ作品を可視化対象としていることにご注意ください。

デフォルト設定のPlotly Expressでは、複数のファセット間で共通のカラースケールを利用します。 上図のようにファセット間でサンプルサイズに違いがあるとき、サンプルサイズが小さい方(アニメ化実績のあるマンガ作品)の配色が不明瞭になってしまいます。 そこで、make_subplots関数を用い、それぞれ独立したカラースケールを持つように修正します。

Hide code cell source
# Plotlyのmake_subplots関数を使って、1行2列のサブプロットを持つ図を作成
# 各サブプロットのタイトルを「アニメ化実績なし」と「アニメ化実績あり」に設定
fig = make_subplots(
    rows=1, cols=2, subplot_titles=["アニメ化実績なし", "アニメ化実績あり"]
)

# x軸とy軸のビンの設定を定義
# x軸は0から1まで、y軸は0からdf_2d[col_y]の最大値の1.1倍までを範囲とする
xbin = dict(start=0, end=1, size=0.1)
ybin = dict(start=0, end=df_2d[col_y].max() * 1.1, size=df_2d[col_y].max() / 10)

# df_2dを'アニメ化'の値でグループ化し、各グループに対して2Dヒストグラムをプロット
# カラースケールの表示を省略し、ビンをそれぞれxbinsとybinsで指定
# 行は必ず1行目、列はアニメ化実績なし=1列目、アニメ化実績あり=2列目
for i, (_, df_tmp) in enumerate(df_2d.groupby("アニメ化")):
    fig.add_trace(
        go.Histogram2d(
            x=df_tmp[col_x],
            y=df_tmp[col_y],
            showscale=False,
            xbins=xbin,
            ybins=ybin,
        ),
        row=1,
        col=i + 1,
    )

# すべてのx軸に対してタイトルをcol_xと設定
fig.update_xaxes(title_text=col_x)
# すべてのy軸に対してタイトルをcol_y設定
fig.update_yaxes(title_text=col_y)

# 作成した図を表示する関数を呼び出し
show_fig(fig)

上図は、マンガ作品の8話目までの平均掲載位置と平均ページ数の分布を、アニメ化実績の有無で分割して表示した二次元ヒストグラムです。 左側がアニメ化実績のない作品、右側がアニメ化実績のある作品の分布です。 色が明るい領域ほど、多くのマンガ作品が該当することを表しますが、配色の基準はファセットごとに独立です。 例えば、左側の図では 150作品 が該当する領域に最も明るい黄色が配色されていますが、右側の図ではわずか 20作品 が該当する領域に同じ色が割り当てられています。 また、1990年以降に連載を開始し、合計8話以上継続したマンガ作品を可視化対象としていることにご注意ください。

アニメ化実績のないマンガ作品(左図)の方が、実績のあるマンガ作品(右図)より平均掲載位置が若干大きい(巻末に寄っている)ように見えますが、大きな違いがあるとは言えません。 また、Y軸、つまり平均ページ数という観点では両者に大きな違いはなさそうに見えます。 以上から、この可視化結果を「連載初期の平均掲載位置と平均ページ数からアニメ化に至るかどうか予測可能である」という仮説を支持する根拠とするのは難しいでしょう。

等値線図#

二次元ヒストグラムでは、X軸とY軸で区分けした長方形の領域を最小単位とするため、分布形状の表現能力に限界があります。 そこで本項では、等値線図を用いて同様の仮説を確認しましょう。

等値線図Density Contours ) は、二種類 の量的変数を対象として、その分布をカーネル密度推定による 曲線 (等値線)で表現する可視化手法です。 二次元ヒストグラムヒストグラムの二次元版だとすると,等値線図は密度プロットの二次元版と捉えることができます。

二次元ヒストグラムと同様、散布図バブルチャートではマーカーが重複してしまうほどデータ量が多いとき、特に効果的です。 二次元ヒストグラムより豊かに分布の形状を表現できますが、等値線はあくまで 推定値 であることに注意が必要です。 詳細は8章にありますので、復習に役立てましょう。

Hide code cell content
# df_2dデータフレームのコピーを作成してdf_contに格納
df_cont = df_2d.copy()

# df_contデータフレームを'マンガ雑誌名'と'アニメ化'で昇順に並び替え、新しいインデックスを割り当て
df_cont = df_cont.sort_values(["マンガ雑誌名", "アニメ化"], ignore_index=True)
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_cont, DIR_OUT, "cont")
DataFrame is saved as '../../data/mix/output/09/assocs/cont.csv'.
Hide code cell source
# Plotly Expressを使用して、df_contデータフレームから密度等値線図を作成
# x軸には変数col_x(最初のmin_nce話までの平均掲載位置)、
# y軸には変数col_y(最初のmin_nce話までの平均ページ数)を設定
# 'アニメ化'ごとに異なる密度等値線図に分け、2列で表示
fig = px.density_contour(
    df_cont, x=col_x, y=col_y, facet_col="アニメ化", facet_col_wrap=2
)

# 密度等値線図のトレース設定を更新
# 等値線の色を塗りつぶしにし、等値線のラベルを表示
fig.update_traces(contours_coloring="fill", contours_showlabels=True)

# 各トレースのカラースケールを非表示に設定
# これはカラーバーの表示が意図しない形になる可能性があるため
fig.update_traces(showscale=False)

# 作成した密度等値線図を表示する関数を呼び出し
show_fig(fig)

上図は、マンガ作品の8話目までの平均掲載位置と平均ページ数の分布を、アニメ化実績の有無で分割して表示した等値線図です。 左側がアニメ化実績のない作品、右側がアニメ化実績のある作品の分布です。 色が明るい領域ほど、多くのマンガ作品が該当することを表します。 なお、1990年以降に連載を開始し、合計8話以上継続したマンガ作品を可視化対象としていることにご注意ください。

繰り返しになりますが、二次元ヒストグラムではX軸とY軸で区分けされた長方形の領域を最小単位とするため、分布形状の分布形状の表現力に限界がありました。 上図では等値線図を採用することで、複雑な分布形状を可視化することに成功しています。

しかし、それでも「連載初期の平均掲載位置と平均ページ数からアニメ化に至るかどうか予測可能である」という仮説を支持する根拠とすることは難しそうに見えます。 確かに両者の分布形状に違いがあるようですが、それぞれ重複する部分も大きく、未知のマンガ作品に対してアニメ化の有無を予測できるとは考えづらいためです。

折れ線グラフ#

ここまでは、一つ一つのマンガ作品に焦点をあてた可視化を行ってきました。 次は、マンガ雑誌巻号に注目し、アニメ化実績のあるマンガ作品が占める割合(アニメ化作品率)について分析してみましょう。 本項では仮に「マンガ雑誌巻号中のアニメ化作品率は大きく変動している」という仮説を立て、折れ線グラフを用いてその妥当性を確認します。

折れ線グラフLine Chart ) は、主に 時間とともに変化する量的変数 を対象に、その量を マーカーの位置 で表現する可視化手法でした。 同様の特徴を持つ散布図との違いは以下の二点です:

  • X軸に日付や時刻等、時間を表現する変数を取ることが多い[2]こと

  • X軸方向に隣接するマーカー同士を直線でつなぎ、系列であることを強調すること

折れ線グラフが優れているのは、直線の 傾き により変化の度合いを表現できる点です。 例えば、急激に量が変化する場合は直線の角度が垂直に近づきますし、緩やかに変化する場合は水平に近づきます。 ただし、上記の前提としてX軸の幅が一定であるという条件があることにご注意ください。

詳細は8章にありますので、復習に役立てましょう。

Hide code cell content
# df_ce_acデータフレームのコピーを作成してdf_tmpに格納
df_tmp = df_ce_ac.copy()

# 'date'列のデータを日付型に変換
df_tmp["date"] = pd.to_datetime(df_tmp["date"])

# n_ceがmin_nce以上かつdateの年が1990年以降のレコードをフィルタリング
df_tmp = df_tmp[
    (df_tmp["n_ce"] >= min_nce) & (df_tmp["date"].dt.year >= 1990)
].reset_index(drop=True)

# 'acid'列に欠損値がない場合にTrueを設定し、新しい列'is_animated'をデータフレームに追加
df_tmp["is_animated"] = ~df_tmp["acid"].isna()
Hide code cell content
# df_tmpデータフレームを'mcname'(マンガ雑誌名)と'date'(発売日)でグループ化し、
# 'is_animated'(アニメ化されているかどうか)に関する統計情報(合計、平均、カウント)を計算
df_line = (
    df_tmp.groupby(["mcname", "date"])["is_animated"]
    .agg(["sum", "mean", "count"])
    .reset_index()
)

# 合計作品数が5を超える行のみを保持
df_line = df_line[df_line["count"] > 5].reset_index(drop=True)

# データフレームの列名をより分かりやすい名称に変更
df_line = df_line.rename(
    columns={
        "mcname": "マンガ雑誌名",
        "date": "発売日",
        "sum": "アニメ化作品数",
        "mean": "アニメ化作品率",
        "count": "合計作品数",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_line.head()
マンガ雑誌名 発売日 アニメ化作品数 アニメ化作品率 合計作品数
0 週刊少年サンデー 1990-01-01 1 0.052632 19
1 週刊少年サンデー 1990-01-02 1 0.050000 20
2 週刊少年サンデー 1990-01-11 1 0.045455 22
3 週刊少年サンデー 1990-01-24 2 0.090909 22
4 週刊少年サンデー 1990-01-31 2 0.100000 20
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_line, DIR_OUT, "line")
DataFrame is saved as '../../data/mix/output/09/assocs/line.csv'.
Hide code cell source
# Plotly Expressを使用して、df_lineデータフレームから折れ線グラフを作成
# x軸には'発売日'、y軸には'アニメ化作品率'を設定し、
# 'マンガ雑誌名'ごとに異なる折れ線グラフに分けて表示
# facet_col_wrap=1により、各マンガ雑誌名ごとに縦に並べて表示
fig = px.line(
    df_line,
    x="発売日",
    y="アニメ化作品率",
    facet_col="マンガ雑誌名",
    facet_col_wrap=1,
    height=500,
)

# ホバーモードを'x'に設定し、同じx位置のデータポイントにマウスを置くと情報が表示されるようにする
fig.update_layout(hovermode="x")

# y軸の範囲を0から1に設定し、アニメ化作品率が0-100%の範囲で表示されるようにする
fig.update_yaxes(range=[0, 1])

# 各ファセットのアノテーション(マンガ雑誌名のラベル)を更新し、
# 不要なプレフィックス("マンガ雑誌名=")を削除して見やすいラベルにする
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# グラフを表示
show_fig(fig)

上図は、マンガ雑誌ごとのアニメ化作品率の推移を表した折れ線グラフです。 アニメ化作品率とは、当該マンガ雑誌巻号に掲載されているマンガ作品のうち、アニメ化に至ったものの割合を示します。 例えば、雑誌巻号に掲載された20作品のうち5作品がアニメ化に至った場合、アニメ化作品率は\(\frac{5}{20}=0.25\)となります。 ただし、ここでアニメ化に至った時期に関しては考慮していないことにご注意ください。 つまり、集計時点でアニメ化されているとは限らず、 将来的に アニメ化される作品や、 過去に アニメ化された作品もこれに含まれます。

それぞれのマンガ雑誌において「マンガ雑誌巻号中のアニメ化作品率は大きく変動している」ことがわかりました。 本書のスコープ外のため割愛しますが、具体的な変動幅はどの程度か、また変動に周期性があるかどうか、等踏み込んだ分析に進む価値があるかもしれません。

なお、各プロットにおいて折れ線が一本だけのとき、折れ線以下の領域を塗りつぶすことで視認性が増すことがあります。 この場合、塗りつぶしの基準として妥当なものを選択することが重要です。 上図の場合は、\(Y=0\)がアニメ化作品が全くない状態を表しますので、折れ線から直線\(Y=0\)までの領域を塗りつぶすことにしましょう。

Hide code cell source
# Plotly Expressを使用して、df_lineデータフレームから積上げ密度プロットを作成
# x軸には'発売日'、y軸には'アニメ化作品率'を設定し、
# 'マンガ雑誌名'ごとに異なる折れ線グラフに分けて表示
# facet_col_wrap=1により、各マンガ雑誌名ごとに縦に並べて表示
fig = px.area(
    df_line,
    x="発売日",
    y="アニメ化作品率",
    facet_col="マンガ雑誌名",
    facet_col_wrap=1,
    height=500,
)

# ホバーモードを'x'に設定し、同じx位置のデータポイントにマウスを置くと情報が表示されるようにする
fig.update_layout(hovermode="x")

# y軸の範囲を0から1に設定し、アニメ化作品率が0-100%の範囲で表示されるようにする
fig.update_yaxes(range=[0, 1])

# 各ファセットのアノテーション(マンガ雑誌名のラベル)を更新し、
# 不要なプレフィックス("マンガ雑誌名=")を削除して見やすいラベルにする
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# グラフを表示
show_fig(fig)

上図は、マンガ雑誌ごとのアニメ化作品率の推移を表した積上げ密度プロットです。 アニメ化作品率とは、当該マンガ雑誌巻号に掲載されているマンガ作品のうち、アニメ化に至ったものの割合を示します。 \(Y=0\)から「折れ線」までの領域を塗りつぶすことで、アニメ化作品率の推移を追いやすくなりました。

連結散布図#

モーションチャートを用いた可視化の結果、アニメ化実績のある作品とそうでない作品では、5話目以降の指標値の挙動に違いがあるように見えました。 そこで本項では、連結散布図を用いて「アニメ化実績のある作品とそうでない作品では、連載開始直後の指標値の推移に違いがある」という仮説を確認します。 これまで設定した仮説と異なり、特定の話数までの平均値ではなく、各話の指標値の 推移 を見ている点にご注意ください。

連結散布図Connected Scatter )とは、縦軸・横軸に変数の値を取り、サンプル付近に時点情報を付加することで、二変数の連続的な変化を表現する可視化手法でした 散布図という名前をしていますが、折れ線グラフの二次元版と考えることもできます。 詳細は8章にありますので、復習に役立てましょう。

Hide code cell content
# df_ce_acデータフレームから、n_ceがmin_nce以上かつfirst_date_ccの年が1990年以降のレコードを選択
df_tmp = df_ce_ac[
    (df_ce_ac["n_ce"] >= min_nce) & (df_ce_ac["first_date_cc"].dt.year >= 1990)
].reset_index(drop=True)

# 各マンガ作品ID(ccid)ごとに、最初のmin_nce話のレコードのみを保持
df_tmp = df_tmp.groupby("ccid").head(min_nce).reset_index(drop=True)

# 'acid'列に欠損値がない場合にTrueを設定し、新しい列'is_animated'をデータフレームに追加
df_tmp["is_animated"] = ~df_tmp["acid"].isna()

# データフレームを'mccid'(マンガ雑誌ID)と'date'(日付)で昇順に並び替え
df_tmp = df_tmp.sort_values(["ccid", "date"], ignore_index=True)

# 各マンガ作品ID(ccid)ごとに連続したエピソード番号(ceno)を割り当て
df_tmp["ceno"] = df_tmp.groupby("ccid").cumcount() + 1

# 各マンガ作品から最初のmin_nce話までのデータのみを保持
df_tmp = df_tmp[df_tmp["ceno"] <= min_nce].reset_index(drop=True)

# 再度、'acid'列に欠損値がない場合にTrueを設定し、'is_animated'列を更新
df_tmp["is_animated"] = ~df_tmp["acid"].isna()
Hide code cell content
# df_tmpデータフレームを'is_animated'と'ceno'(エピソード番号)でグループ化し、
# 平均掲載位置、平均ページ数、カラー獲得率、作品数を集計
df_conn = (
    df_tmp.groupby(["is_animated", "ceno"])
    .agg(
        平均掲載位置=("page_start_position", "mean"),
        平均ページ数=("pages", "mean"),
        カラー獲得率=("four_colored", "mean"),
        作品数=("ceno", "size"),
    )
    .reset_index()
)

# 'ceno'列の値を使用して、各エピソード番号に対応するラベル(例:"1話目")を割り当て
df_conn["text"] = df_conn["ceno"].map(lambda x: f"{x}話目")

# データフレームの列名をより分かりやすい名称に変更
# 'is_animated'を'アニメ化'に、'ceno'を'話数'にリネーム
df_conn = df_conn.rename(columns={"is_animated": "アニメ化", "ceno": "話数"})
Hide code cell content
# 可視化対象のDataFrameを確認
df_conn.head()
アニメ化 話数 平均掲載位置 平均ページ数 カラー獲得率 作品数 text
0 False 1 0.155210 46.352649 0.769868 1208 1話目
1 False 2 0.294062 29.057119 0.230960 1208 2話目
2 False 3 0.379118 21.525662 0.100993 1208 3話目
3 False 4 0.418088 18.772351 0.083609 1208 4話目
4 False 5 0.468384 17.968543 0.067881 1208 5話目
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_conn, DIR_OUT, "conn")
DataFrame is saved as '../../data/mix/output/09/assocs/conn.csv'.
Hide code cell source
# create_connectedplotを使って連結散布図を作成
# x軸には'平均掲載位置'、y軸には'カラー獲得率'を設定し、マーカーの色には'話数'を設定
# 'アニメ化'の値によって異なるグラフに分けて表示
fig = create_connectedplot(
    df_conn,
    x="平均掲載位置",
    y="カラー獲得率",
    color="話数",
    text="text",
    facet_col="アニメ化",
)

# x軸とy軸の範囲を設定
fig.update_xaxes(range=[-0.05, 1.05])
fig.update_yaxes(range=[-0.05, 1.05])

# 作成した図を表示する関数を呼び出し
show_fig(fig)

上図は、アニメ化実績のあるマンガ作品とそうでない作品の、平均的な掲載位置とカラー獲得率の推移を表示した連結散布図です。 1話目から8話目までを可視化対象としており、マーカーの色が明るいほど話数が大きいことを表します。 具体的には1話目は暗い紫色で、8話目は明るい黄色で表現されています。

アニメ化に至らなかったマンガ作品(左図)においては、話数を重ねるごとに平均掲載位置は大きくなり、カラー獲得率も\(0\)付近まで落ち込みます。 特に1話から3話にかけてのカラー獲得率の落ち込みが急激ですので、結果としてアイスホッケーのスティックのような形状[^difference]になりました。

一方でアニメ化に至ったマンガ作品(右図)においては、3話目まで同様の挙動を示すのですが、4話目以降にマーカーが「詰まって」いるように見えます。 これは平均掲載位置が\(0.4\)以上に増えていないことや、カラー獲得率が\(0\)付近で停滞せず上下に動くことが原因です。 連結散布図として二つの指標の推移を可視化することで、二つのグループの違いの一端が明らかになったと言えるのではないでしょうか。

以上から「アニメ化実績のある作品とそうでない作品では、連載開始直後の指標値の推移に違いがある」という仮説は、全くの見当違いではなさそうです。

では、マンガ雑誌ごとに推移に違いが出るのでしょうか?可視化してみましょう。

Hide code cell content
# df_tmpデータフレームを'mcname'(マンガ雑誌名)、'is_animated'(アニメ化されているかどうか)、
# 'ceno'(エピソード番号)でグループ化し、平均掲載位置、平均ページ数、カラー獲得率、作品数を集計
df_conn2 = (
    df_tmp.groupby(["mcname", "is_animated", "ceno"])
    .agg(
        平均掲載位置=("page_start_position", "mean"),
        平均ページ数=("pages", "mean"),
        カラー獲得率=("four_colored", "mean"),
        作品数=("ceno", "size"),
    )
    .reset_index()
)

# 'ceno'の値を使用して各エピソード番号に対応するラベル(例:"1話目")を割り当て
df_conn2["text"] = df_conn2["ceno"].map(lambda x: f"{x}話目")

# データフレームの列名をより分かりやすい名称に変更
# 'mcname'を'マンガ雑誌名'に、'is_animated'を'アニメ化'に、'ceno'を'話数'にリネーム
df_conn2 = df_conn2.rename(
    columns={
        "mcname": "マンガ雑誌名",
        "is_animated": "アニメ化",
        "ceno": "話数",
    }
)
Hide code cell content
# 可視化対象のDataFrameを確認
df_conn2.head()
マンガ雑誌名 アニメ化 話数 平均掲載位置 平均ページ数 カラー獲得率 作品数 text
0 週刊少年サンデー False 1 0.145375 42.392157 0.725490 255 1話目
1 週刊少年サンデー False 2 0.276159 26.811765 0.168627 255 2話目
2 週刊少年サンデー False 3 0.323798 20.584314 0.125490 255 3話目
3 週刊少年サンデー False 4 0.355677 18.094118 0.129412 255 4話目
4 週刊少年サンデー False 5 0.390275 17.494118 0.098039 255 5話目
Hide code cell content
# 可視化対象DataFrameを保存
save_df_to_csv(df_conn2, DIR_OUT, "conn2")
DataFrame is saved as '../../data/mix/output/09/assocs/conn2.csv'.
Hide code cell source
# create_connectedplotを使って連結散布図を作成
# x軸には'平均掲載位置'、y軸には'カラー獲得率'を設定し、マーカーの色には'話数'を設定
# 'アニメ化'の値によって列を分け、'マンガ雑誌名'によって行を分けてファセットを作成し、高さを調整
fig = create_connectedplot(
    df_conn2,
    x="平均掲載位置",
    y="カラー獲得率",
    color="話数",
    text="text",
    facet_col="アニメ化",
    facet_row="マンガ雑誌名",
    height=800,
)

# x軸とy軸の範囲を設定
fig.update_xaxes(range=[-0.05, 1.05])
fig.update_yaxes(range=[-0.05, 1.05])

# 各ファセットのアノテーション(タイトル)を更新し、不要なプレフィックスを削除
fig.for_each_annotation(lambda a: a.update(text=a.text.split("=")[-1]))

# 作成した図を表示する関数を呼び出し
show_fig(fig)

上図は、マンガ作品の平均掲載位置とカラー獲得率の推移を、マンガ雑誌(行方向)とアニメ化実績の有無(列方向)でファセットに分けて表示した連結散布図です。 1話目から8話目までを可視化対象としており、マーカーの色が明るいほど話数が大きいことを表します。 具体的には1話目は暗い紫色で、8話目は明るい黄色で表現されています。

マンガ雑誌別に細かな違いはありますが、

  • アニメ化実績のないマンガ作品は、話数を重ねるごとに平均掲載位置が巻末に寄り続け、カラー獲得率は\(0\)付近に収束する

  • アニメ化実績のあるマンガ作品は、特定の話数で上記の流れに逆らうような動きを見せる

という点は共通しているように見えます。